//A module that allows you to drag-and-drop rows in grid.  This module is compatible with
//the TreeGridModule.
function DraggableRowsModule() {

	var base = this;

	//Hooks in to rowBound event so that rows can be made draggable and droppable.
	base.initialize = function(liteGrid, options) {

		base.options = options;
		base.liteGrid = liteGrid;

		//See if the TreeGridModule is loaded.
		var treeGrid = liteGrid.getModule("TreeGridModule");
		if (treeGrid != null) {
			base.treeGrid = treeGrid;
		}

		//Find the utils column
		$(options.columns).each(function(index) {
			if (this.field == "utils") {
				base.utilsIndex = index;
			}
		});

		if (base.utilsIndex === undefined) {
			console.error("Unable to find a column named 'utils' to add the tree controls to. ");
			return;
		}

		//Make sure jQuery UI is loaded.
		if (!$.fn.draggable || !$.fn.droppable) {
			console.error("Unable to load DraggableRowModule because jQuery UI is not available.");
			return;
		}

		//Initialize module-specific options
		base.options = $.extend({}, DraggableRowsModule.prototype.defaultOptions, options);

		//Register to receive rowBound events, that way we can insert the drag handle.
		liteGrid.$el.bind("rowBound", base.rowBound);
	}
	
	//Gets the expander cell from the specified row.
	base.getUtilsCell = function(row) {
		return row.children(":eq(" + base.utilsIndex + ")");
	}

	//Makes rows drag and drop compatible.
	base.rowBound = function(event, row, dataItem, index) {

		//Add the drag handle to the utils column.
		var dragHandle = $("<span class='draghandle ui-icon ui-icon-grip-dotted-vertical'/>");

		if (base.treeGrid) {
			base.getUtilsCell(row).find("span.expander").before(dragHandle);
		}
		else {
			base.getUtilsCell(row).append(dragHandle);
		}

		dragHandle.draggable(
		{
			helper: function() { return "<div class='ghost'></div>"; },
			start: base.resizeGhost,
			drag: base.dragGhost,
			revert: 'invalid',
			handle: 'span.draghandle',
			cursorAt: { left: 0, top: 0 },
			refreshPositions: true
		});

		row.droppable({
			hoverClass: 'ui-state-active',
			drop: base.rowDropped
		});
	}
	
	//Resizes the ghost to the same size as the row.
	base.resizeGhost = function(event, ui) {

		var helper = ui.helper;
		//The actual target is the drag handle, but we want the row.
		var row = $(event.target).parents("tr");

		//Calculate the height of the row and its visible children.
		var rowId = row.data("dataItem")[base.options.rowIdColumn];

		var rowsToMove = row.add(base.liteGrid.getChildren(rowId));

		var height = 0;

		rowsToMove.each(function() {
			var child = $(this);
			if (child.css("display") != "none") {
				height += child.height();
			}
		});

		helper.width(row.width());
		helper.height(height);

		//Center the helper on the cursor.  This makes the UI a little more intuitive.
		helper.css("margin-top", -1 * height / 2);
		helper.css("line-height", height + "px");
	}

	//Adds a helpful message to the ghost during dragging.
	base.dragGhost = function(event, ui) {

		var dragHandle = $(event.target);

		//If dragging caused a scroll, the positions will be refreshed.
		if (dragHandle.draggable('option', 'refreshPositions')) {
			dragHandle.draggable('option', 'refreshPositions', false)
		}

		//If we're going to add the row as a child
		if (ui.position.left > base.options.draggablePixelThreshold) {
			if (base.treeGrid) {
				ui.helper.html("Drop to add as a subtree.");
			}
			else {
				ui.helper.html("Drop to add after row.");
			}
		}
		else {
			ui.helper.html("Drop to add before row.");
		}

		//If we're dragging outside of the grid, scroll.  We can't
		//use the built-in scroll capabilities of draggable because 
		//its not the table that we need to scroll, but rather the parent. 
		var body = base.liteGrid.bodyDiv;
		var bodyOffset = body.offset();
		var bodyHeight = body.height();
		var currentScroll = body.attr("scrollTop");

		var rowOffset = ui.helper.offset()

		//If we're above the body div, scroll up.
		if (rowOffset.top < bodyOffset.top) {
			dragHandle.draggable('option', 'refreshPositions', true);
			body.attr("scrollTop", currentScroll - base.options.dragIncrement);
		}
		else if (rowOffset.top > (bodyOffset.top + bodyHeight)) {
			dragHandle.draggable('option', 'refreshPositions', true);
			body.attr("scrollTop", currentScroll + base.options.dragIncrement);
		}
	}

	//Fired when a row is dropped.
	base.rowDropped = function(event, ui) {

		var target = $(event.target);
		var row = ui.draggable.parents("tr");

		var dataItem = row.data("dataItem");

		var rowId = dataItem[base.options.rowIdColumn];

		var toMove = row.add(base.liteGrid.getChildren(rowId));

		//If the target row is a child of the row that was moved (or the row itself), don't do anything.
		if (!base.moveAllowed(toMove, target)) {
			return;
		}

		//If the mouse is more than 50 pixels to the left of where it started, assume
		//they want to add it as a child of the target row.
		if (ui.position.left > base.options.draggablePixelThreshold) {
			base.addRowsAsChildren(toMove, target);
		}
		else {
			base.moveRowsBefore(toMove, target);
		}

		base.ensureRowVisibile(row);

		base.pulseVisibleRows(toMove);

		//Signal that the table has changed.
		base.liteGrid.$el.trigger("tableUpdated", base.liteGrid);
	}

	//Checks to make sure the move is valid and allows interested parties to 
	//intercept and cancel the move if they want.
	base.moveAllowed = function(rows, destinationRow) {

		//Make sure we're not trying to move rows to themselves.
		destinationRow = destinationRow.get(0);
		for (var i = 0; i < rows.length; i++) {
			if (rows.get(i) == destinationRow) {
				return false;
			}
		}

		//Alert interested parties that we're going to move the row and allow them to cancel it.
		var rowsMoving = $.Event("RowsMoving");
		rowsMoving.rows = rows;
		rowsMoving.destination = $(destinationRow);
		rowsMoving.cancelMove = false;

		base.liteGrid.$el.trigger(rowsMoving);

		return rowsMoving.cancelMove !== true;
	}
	
	//Moves the specified rows under the specified parent (or after if we're not in treegrid mode).
	base.addRowsAsChildren = function(rows, parentRow) {

		rows.insertAfter(parentRow);

		if (!base.treeGrid) {
			return;
		}

		base.adjustRowParent(rows.eq(0), parentRow);

		base.reapplyIndent(rows, parentRow);

		//If the row we added to was not previously expandable, we have to make it expandable.
		var expander = base.getUtilsCell(parentRow).find("span.expander");

		if (expander.hasClass("ui-icon-bullet")) {

			var dataItem = parentRow.data("dataItem");

			parentRow.addClass("children-loaded")
					 .attr("expanded", true);

			expander.addClass("ui-icon-minusthick")
					   .removeClass("ui-icon-bullet")
					   .toggle(function() { base.treeGrid.rowCollapsed(dataItem[base.options.rowIdColumn]); },
							   function() { base.treeGrid.rowExpanded(dataItem[base.options.rowIdColumn]); }
							 );
		}
		else if (expander.hasClass("ui-icon-plusthick") && !expander.hasClass("childrenLoaded")) {
			expander.click();
		}
	}

	//Attaches the row under the specified parent.  If newParent is null, the row
	//is attached as a root.  This doesn't actually manipulate the DOM, it just restores
	//underlying relationships.
	base.adjustRowParent = function(row, newParent) {

		var currentParentId = row.attr("parentid");
		var newParentId = newParent != null ? newParent.data("dataItem")[base.options.rowIdColumn] : base.options.rootId;

		//Strip the current parent/child info from the row.
		if (currentParentId != undefined) {
			row.removeAttr("parentid")
					.removeClass("child-of-" + currentParentId);
		}

		//Add an entry to the row's dataItem so that the move can be handled server-side.
		row.data("dataItem")[base.options.newIdColumn] = newParentId == undefined ? base.options.rootId : newParentId;

		if (newParentId != undefined) {
			row.attr("parentid", newParentId)
					.addClass("child-of-" + newParentId);
		}

		//Mark the row as changed.
		base.getUtilsCell(row).addClass("modified ui-state-highlight");

		//Mark the parent as having children.  This is needed incase the row is rebound. 
		if (newParent != null) {
			newParent.data("dataItem").HasChildren = true;
		}
	}

	//Adjust the indent on all the rows so that they are correct based on their new 
	//location.
	base.reapplyIndent = function(movedRows, newParent) {

		var amountToIndent = 0;

		if (newParent != null) {
			amountToIndent = parseInt(base.getUtilsCell(newParent).find("span.expander").css("margin-left"));
			if (isNaN(amountToIndent)) {
				amountToIndent = 0;
			}

			//Indent one more level than the parent.
			amountToIndent += base.treeGrid.options.paddingPerLevel;
		}

		var firstRow = movedRows.eq(0);

		var currentIndention = parseInt(base.getUtilsCell(firstRow).find("span.expander").css("margin-left"));
		if (isNaN(currentIndention)) {
			currentIndention = 0;
		}

		var offset = amountToIndent - currentIndention; //How much to shift all the rows.

		movedRows.each(function() {

			var row = $(this);
			
			var expander = base.getUtilsCell(row).find("span.expander");

			var currentIndention = parseInt(expander.css("margin-left"));
			if (isNaN(currentIndention)) {
				currentIndention = 0;
			}

			var expanderPadding = (currentIndention + offset) + "px";

			expander.css("margin-left", expanderPadding);

			row.attr("expanderPadding", expanderPadding);
		});

	}
	
	//Moves the specified rows to immediately before the specified destination.
	base.moveRowsBefore = function(rows, destinitionRow) {

		//Place the rows before the target. 
		$(rows).insertBefore(destinitionRow);

		//Update things like parent/child relationships, indention, etc, but only
		//if the tree grid module is loaded.
		if (!base.treeGrid) {
			return;
		}

		var firstRow = rows.eq(0);

		var parentId = destinitionRow.attr("parentid");
		var parent = null;

		if (parentId != undefined && parentId != base.options.rootId) {
			parent = $("tr[id='row-id-" + parentId + "']", base.liteGrid.$el);
		}

		base.adjustRowParent(firstRow, parent);

		base.reapplyIndent(rows, parent);
	}
	
	//Pulses any rows that are not hidden.
	base.pulseVisibleRows = function(rows) {

		var toPulse = $([]);

		rows.each(function() {
			var row = $(this);
			if (!row.hasClass("hidden")) {
				toPulse = toPulse.add(row);
			}
		});

		toPulse.stop();
		toPulse.effect("highlight", {}, 2000);
	}

	//Ensure that the row is visible by scrolling to it.
	base.ensureRowVisibile = function(row) {
	
		//Make sure the row is visible
		var currentScroll = base.liteGrid.bodyDiv.attr("scrollTop");
		//The position doesn't account for the current scroll position, so you have to treat it as an offset.
		base.liteGrid.bodyDiv.attr("scrollTop", currentScroll + row.position().top - 2 * row.height());
	}
}
DraggableRowsModule.prototype.defaultOptions = {
	//Number of pixels from the left edge that determines when to add before vs. add under/after.
	draggablePixelThreshold: 50,
	//The name to use for setting the ID of a new parent when a part is moved. 
	newIdColumn: "newParentId",
	//The ID to use when a row is moved to the root
	rootId: 0,
	//The amount in pixels to scroll the grid when dragging outside of the viewable area of the grid.
	dragIncrement: 10
};